/*
 * Todo:
 *  - dodělat pluginy
 *  - nastavit security pro pluginy
 *  - povolit reakce na multiple objecty. Tj. pokud existuje X1, X2 - měli by odpovídat společně na název X
 *  - tisk
 *  - načtení, uložení
 * Bugy:
 *  - umistovac popisku funguje divne vpravo a nahore.
 */
package cz.hamacekj.geometr;

import cz.hamacekj.geometr.internalplugins.AnglePlugin;
import cz.hamacekj.geometr.internalplugins.CirclePlugin;
import cz.hamacekj.geometr.internalplugins.LinePlugin;
import cz.hamacekj.geometr.internalplugins.PointPlugin;
import cz.hamacekj.geometr.internalplugins.RayPlugin;
import cz.hamacekj.geometr.internalplugins.SegmentPlugin;
import cz.hamacekj.geometr.plugin.ConstructionObject;
import cz.hamacekj.geometr.plugin.ConstructionPlugin;
import cz.hamacekj.geometr.plugin.IPoint;
import cz.hamacekj.geometr.plugin.IPointObject;
import cz.hamacekj.geometr.plugin.IToolBox;
import cz.hamacekj.geometr.plugin.InputParametersInterface;
import cz.hamacekj.geometr.plugin.ParameterItem;
import cz.hamacekj.geometr.plugin.ParameterObject;
import cz.hamacekj.geometr.plugin.ValueArgument;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.print.PageFormat;
import java.awt.print.PrinterException;
import java.awt.print.PrinterJob;
import java.io.File;
import java.io.FileFilter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.ServiceLoader;
import java.util.concurrent.ExecutionException;
import javax.print.attribute.Attribute;
import javax.print.attribute.HashPrintRequestAttributeSet;
import javax.print.attribute.PrintRequestAttributeSet;
import javax.print.attribute.standard.OrientationRequested;
import javax.swing.SwingWorker;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

/**
 * Vstupní bod programu. Realizuje controller část MVC.
 * @author hamacekh
 */
public class Geometr {

    public Geometr() {
        // load all plugins
        ToolBox toolbox = new ToolBox();
        final ConstructionPlugin[] internalPlugins;
        if(false){ 
            ConstructionPlugin[] tmp = { new SegmentPlugin(),
            new CirclePlugin(), new PointPlugin(toolbox), new LinePlugin(toolbox),
        new RayPlugin(toolbox), new AnglePlugin(toolbox) };
            internalPlugins = tmp;
        }else{
            internalPlugins = new ConstructionPlugin[0];
        }
        ui = new UI();
        steps = new StepsModel();
        steps.addListDataListener(new StepsChangedListener());
        constructionPlugins = new HashMap<>();
        ui.setStepModel(steps);
        // register plugins to gui
        for(final ConstructionPlugin p : internalPlugins){
            this.constructionPlugins.put(p.getPluginName(), p);
        }
        final ArrayList<ConstructionPlugin> externalPlugins = new ArrayList<>();
        for(ConstructionPlugin p : loadExternalPlugins()){
            externalPlugins.add(p);
        }
        for(final ConstructionPlugin p : externalPlugins){
            try{
                Method m = p.getClass().getMethod("setToolbox", IToolBox.class);
                m.invoke(p, toolbox);
            }catch(NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException exc){}
            this.constructionPlugins.put(p.getPluginName(), p);
        }
        
        // CreateAndShowGUI
        try{
            javax.swing.SwingUtilities.invokeAndWait(new Runnable() {

                @Override
                public void run() {
                    ui.createAndShowGUI();
                    // listen to constructed stuff
                    ui.addConstructObjectListener(new NewObjectListener());
                    ui.addStepSelectionListener(new StepSelectedListener());
                    for(ConstructionPlugin p : internalPlugins){
                        ui.addConstructionObject(p.getPluginName(), p.getIcon(), p.getPossibleParameters());
                    }
                    for(ConstructionPlugin p : externalPlugins){
                        ui.addConstructionObject(p.getPluginName(), p.getIcon(), p.getPossibleParameters());
                    }
                    ui.addStepSelectionListener(new ListSelectionListener() {

                        @Override
                        public void valueChanged(ListSelectionEvent e) {
                            ui.repaint();
                        }
                    });
                    ui.addDeleteLastListener(new DeleteLastListener());
                    ui.addSaveListener(new SaveListener());
                    ui.addOpenListener(new OpenListener());
                    ui.addBlankListener(new ClearStepsListener());
                    ui.addPrintListener(new PrintListener());
                    ui.addPrintListListener(new PrintListListener());
                    refreshHiddenParameters();
                }
            });
        }catch(InterruptedException | InvocationTargetException e){
            System.exit(1);
        }
    }
    
    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        Geometr g = new Geometr();
    }
    
    private class DeleteLastListener implements ActionListener{

        @Override
        public void actionPerformed(ActionEvent e) {
            int size = steps.getSize();
            if(size == 0){
                return;
            }
            if(size != 1 && ui.getSelectedStepIndex() == size-1){
                ui.setSelectedStep(size - 2);
            }
            steps.deleteElementAt(size - 1);
            refreshHiddenParameters();
            ui.repaint();
        }
        
    }
    
    private class OpenListener implements ActionListener{

        @Override
        public void actionPerformed(ActionEvent e) {
            if(steps.getSize() > 0 && unsavedChanges){
                boolean res = ui.showQuestion("Opravdu přemazat?",
                        "Otevřením ulženého dokumentu ztratíte neuložené změny."+System.lineSeparator()
                        +"Opravdu chcete pokračovat?");
                if(!res){
                    return;
                }
            }
            final File f = ui.showLoadDialog();
            if(f == null){
                return;
            }
            SwingWorker<String, String> worker = new SwingWorker<String, String>() {

                @Override
                protected String doInBackground() throws Exception {
                    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
                    DocumentBuilder builder = factory.newDocumentBuilder();
                    Document doc = builder.parse(f);
                    Element root = doc.getDocumentElement();
                    if(!"geometr".equals(root.getNodeName())){
                        return "Soubor, který se snažíte otevřít je poškozený.";
                    }
                    NodeList list = root.getChildNodes();
                    for(int i = 0; i < list.getLength(); i++){
                        if(list.item(i).getNodeType() != Element.ELEMENT_NODE
                                || !"steps".equals(list.item(i).getNodeName())){
                            continue;
                        }
                        steps.fillFromDOMElement((Element)list.item(i), constructionPlugins);
                        break;
                    }
                    return null;
                }

                @Override
                protected void done() {
                    try{
                        String error = get();
                        if(error != null){
                            ui.showError("Chyba otvírání", error);
                        }
                        ui.setSelectedStep(steps.getSize() - 1);
                        refreshHiddenParameters();
                        unsavedChanges = false;
                    }catch(ExecutionException e){
                        e.printStackTrace(System.err);
                        ui.showError("Chyba otvírání", "Otvíraný soubor je poškozeny.");
                    }catch(InterruptedException e){}
                    
                }
            };
            worker.execute();
        }
        
    }
    
    /**
     * Listener k uložení současného postupu konstrukce. Pamatuje si poslední místo
     * uložení a ukládá na něj.
     */
    private class SaveListener implements ActionListener {
        private File savePath = null;

        public SaveListener() {
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            if(savePath == null || "SaveAs".equals(e.getActionCommand())){
                savePath = ui.showSaveDialog();
            }
            if(savePath == null){
                return;
            }
            SwingWorker<String, String> worker = new SwingWorker<String, String>() {

                @Override
                protected String doInBackground() throws Exception {
                    DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
                    DocumentBuilder builder = builderFactory.newDocumentBuilder();
                    Document doc = builder.newDocument();
                    Element root = doc.createElement("geometr");
                    root.setAttribute("version", "1");
                    doc.appendChild(root);
                    root.appendChild(steps.createDOMElement(doc));
                    TransformerFactory factory = TransformerFactory.newInstance();
                    Transformer trans = factory.newTransformer();
                    DOMSource source = new DOMSource(doc);
                    StreamResult result = new StreamResult(savePath);
                    trans.transform(source, result);
                    return null;
                }

                @Override
                protected void done() {
                    unsavedChanges = false;
                    ui.showMessage("Uloženo", "Postup byl úspěšně uložen");
                }
            };
            worker.execute();
        }
        
    }
    
    private class ClearStepsListener implements ActionListener {

        @Override
        public void actionPerformed(ActionEvent e) {
            if(unsavedChanges && steps.getSize() > 0){
                boolean res = ui.showQuestion("Neuložené změny",
                        "Všechny neuložené změny budou ztraceny"+System.lineSeparator()+
                        "Pokračovat?");
                if(!res){
                    return;
                }
            }
            steps.clear();
            refreshHiddenParameters();
            ui.repaint();
        }
        
    }
    
    private class PrintListener implements ActionListener {

        @Override
        public void actionPerformed(ActionEvent e) {
            ConstructionPainter painter = new ConstructionPainter(steps,
                    ui.stepsSelection, ui.painter.getLengthRatio(),
                    ui.painter.getRelativeStart());
            PrintSteps p = new PrintSteps(painter);
            PrinterJob job = PrinterJob.getPrinterJob();
            job.setPrintable(p);
            int orientation = ui.painter.getPageFormat().getOrientation();
            Attribute o = orientation == PageFormat.LANDSCAPE ? OrientationRequested.LANDSCAPE : OrientationRequested.PORTRAIT;
            PrintRequestAttributeSet attributes = new HashPrintRequestAttributeSet();
            attributes.add(o);
            boolean doPrint = job.printDialog(attributes);
            if(doPrint){
                try{
                    job.print();
                }catch(PrinterException ex){
                    ex.printStackTrace(System.err);
                    ui.showError("Chyba při tisku", "Nepodařilo se vytisknout požadovanou stránku.");
                }
            }
        }
    }
    
    private class PrintListListener implements ActionListener {

        @Override
        public void actionPerformed(ActionEvent e) {
            PrintStepList list = new PrintStepList(ui.stepsListView, ui.constructionObjects);
            PrinterJob job = PrinterJob.getPrinterJob();
            job.setPrintable(list);
            boolean doPrint = job.printDialog();
            if(doPrint){
                try{
                    job.print();
                }catch(PrinterException ex){
                    ex.printStackTrace(System.err);
                    ui.showError("Chyba při tisku", "Nepodařilo se vytisknout požadovanou stránku.");
                }
            }
        }
        
    }
    
    private class StepSelectedListener implements ListSelectionListener{

        @Override
        public void valueChanged(ListSelectionEvent e) {
            // tell UI to repaint
            ui.repaint();
        }
        
    }
    
    
    private class NewObjectListener implements ConstructObjectListener{

        @Override
        public void objectConstructed(ConstructObjectEvent e) {
            String pluginName = e.getName();
            ConstructionPlugin p = constructionPlugins.get(pluginName);
            ValueArgument[] valueArguments = e.getValueArguments();
            
            // compute valuearguments
            for(int i = 0; i < valueArguments.length; i++){
                String val = valueArguments[i].getRealInput();
                DefaultValueArgument v = new DefaultValueArgument();
                v.setRealInput(valueArguments[i].getRealInput());
                v.setArgumentName(valueArguments[i].getArgumentName());
                try{
                    double intVal = Double.parseDouble(val);
                    v.setValue(intVal);
                    valueArguments[i] = v;
                }catch(NumberFormatException ex){
                    // je potřeba zjistit o jaké dva objekty (body) jde a zjistit jejich vzdálenost.
                    try{
                        if(val.charAt(0) == '|'){
                            val = val.substring(1);
                        }
                        String a = getFirstName(val);
                        String b = getFirstName(val.substring(a.length()));
                        if(!a.isEmpty() && !b.isEmpty() &&
                                val.substring(a.length()+b.length()).length() < 2){
                            IPointObject A = (IPointObject) steps.getObjectByName(a)
                                    .getPrimitiveObjects().iterator().next();
                            IPointObject B = (IPointObject) steps.getObjectByName(b)
                                    .getPrimitiveObjects().iterator().next();
                            v.setValue(dist(A,B));
                        }
                    }catch(NullPointerException | ClassCastException | StringIndexOutOfBoundsException exe){
                        // pokud objekty neexistuji nebo to nejsou body,
                        // ConstructionPlugin dostane realValue NaN.
                        v.setValue(Double.NaN);
                    }
                }
                valueArguments[i] = v;
            }
            // najit typ parametru ktery je pouzivan
            InputParametersInterface[] possibleParams = p.getPossibleParameters();
            InputParametersInterface picked = null;
            for (InputParametersInterface par: possibleParams){
                if(par.getParametersCaption().equals(e.getParametersCaption())){
                    picked = par;
                    break;
                }
            }
            // prevest nazvy objektu na objekty
            String[] objectNames = e.getObjectArguments();
            ArrayList<ParameterObject> parameterObjects = new ArrayList<>();
            for(ParameterItem pi : picked.getParameters()){
                if(pi instanceof ParameterObject){
                    parameterObjects.add((ParameterObject)pi);
                }
            }
            ConstructionObject[] objects = new ConstructionObject[objectNames.length];
            for(int i = 0; i < objectNames.length; i++){
                ParameterObject object = parameterObjects.get(i);
                String[] acceptedObjects = object.getObjectNames();
                if(acceptedObjects == null){
                    objects[i] = steps.getObjectByName(objectNames[i]);
                    if(objects[i] == null){
                        ui.showError("Chyba při tvorbě objektu", "Pole "+object.getCaption()+" je špatně vyplněno."
                                +System.lineSeparator()+"žádný objekt se jménem "+objectNames[i]+
                                " není nakreslen.");
                        return;
                    }
                }else{
                    objects[i] = steps.getObjectByName(objectNames[i]);
                    if(objects[i] == null){
                        ui.showError("Chyba při tvorbě objektu", "Pole "+object.getCaption()+" je špatně vyplněno."
                                +System.lineSeparator()+"žádný objekt se jménem "+objectNames[i]+
                            " jednoho z typů "+strJoin(acceptedObjects, ", ")+
                                " není nakreslen.");
                        return;
                    }
                }
            }
            // call coresponding ConstructionPlugin and get ConstructionObject
            ConstructionObject[] newObjects = null;
            try{
                newObjects = p.createConstructionObject(picked, objects, valueArguments);
            }catch(Exception exc){
                exc.printStackTrace(System.out);
                ui.showError("Chyba při tvorbě objektu", ""
                        + "Nebylo možné nakreslit zadaný objekt. Zkontrolujte, "
                        + "jestli jste zadali správné vstupní parametry.");
                throw exc;
            }
            if(newObjects == null || newObjects.length == 0){
                String error = p.getError();
                if(error == null){
                    ui.showError("Chyba při tvorbě objektu", "Nepodařilo se vytvořit objekt se zadanými parametry");
                }else{
                    ui.showError("Chyba při tvorbě objektu", error);
                }
                return;
            }
            // create Step and add it to ConstructionListModel
            Step newStep = new Step(newObjects, picked, p);
            steps.addStep(newStep);
            int length = steps.getSize();
            ui.setSelectedStep(length - 1);
            refreshHiddenParameters();
            
            javax.swing.SwingUtilities.invokeLater(new Runnable() {

                @Override
                public void run() {
                    ui.repaint();
                }
            });
        }
        
        private double dist(IPointObject a, IPointObject b){
            IPoint x = a.getPoint();
            IPoint y = b.getPoint();
            return Math.sqrt(square(x.getX() - y.getX()) + square(x.getY() - y.getY()));
        }
        
        private double square(double x){
            return x*x;
        }
        
        private String getFirstName(String s){
            if(Character.isUpperCase(s.charAt(0))){
                int i = 1;
                while(s.length() > i && !Character.isUpperCase(s.charAt(i)) && s.charAt(i) != '|'){
                    i++;
                }
                return s.substring(0, i);
            }
            return null;
        }
        
    }
    
    /**
     * Listener, který poslouchá změny na postupu konstrukce.
     */
    private class StepsChangedListener implements ListDataListener{

        @Override
        public void intervalAdded(ListDataEvent e) {
            unsavedChanges = true;
        }

        @Override
        public void intervalRemoved(ListDataEvent e) {
            unsavedChanges = true;
        }

        @Override
        public void contentsChanged(ListDataEvent e) {
            unsavedChanges = true;
        }
        
    }
    
    /**
     * Aktualizuje seznam skrytych/zobrazenych objektu v toolbaru.
     */
    private void refreshHiddenParameters(){
        // aktualizuj seznam skrytych/zobrazenych objektu v toolbaru
        for (ConstructionPlugin pl: constructionPlugins.values()){
            for (InputParametersInterface args: pl.getPossibleParameters()){
                if((args.getMaxStepsToShow() == -1 || steps.getSize() <= args.getMaxStepsToShow())
                        && steps.getSize() >= args.getMinStepsToShow()){
                    ui.showInputParameters(pl.getPluginName(), args.getParametersCaption());
                }else{
                    ui.hideInputParameters(pl.getPluginName(), args.getParametersCaption());
                }
            }
        }
    }
    
    private Iterable<ConstructionPlugin> loadExternalPlugins(){
        File loc = new File("plugins");
        File[] files = loc.listFiles(new FileFilter() {

            @Override
            public boolean accept(File pathname) {
                return pathname.isFile();
            }
        });
        URL[] urls = new URL[files.length];
        try{
            for (int i = 0; i < files.length; i++){
                urls[i] = files[i].toURI().toURL();
            }
        }catch(MalformedURLException e){}
        URLClassLoader cl = new URLClassLoader(urls);
        ServiceLoader<ConstructionPlugin> sl = ServiceLoader.load(ConstructionPlugin.class, cl);
        return sl;
    }
    
    private String strJoin(String[] str, String delim){
        StringBuilder res = new StringBuilder(str.length * 2);
        for(int i = 0; i < str.length; i++){
            res.append(str[i]);
            if(i != str.length - 1){
                res.append(delim);
            }
        }
        return res.toString();
    }
    
    
    private boolean unsavedChanges = false;
    /** Pole načtených pluginů */
    private HashMap<String, ConstructionPlugin> constructionPlugins;
    private StepsModel steps;
    private UI ui;
}
